﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using regxExtensions;

namespace CmdTailCmd
{
    /// <summary>
    /// This is the "context" of a parsed command tail: the original tokens,
    /// any exceptions, and any left-over bits. It's everything *except* the
    /// parsed results.
    /// </summary>
    public class CmdTailContext
    { //======================================================================
        /// <summary>
        /// All exceptions encountered when parsing. If IgnoreAllErrors was set
        /// on the CmdTail object, this may contain any number or type of CmdTailException
        /// objects
        /// </summary>
        public  IEnumerable<CmdTailException>    ParsingExceptions  {get {return _ParsingExceptions.AsEnumerable();}}
        private List<CmdTailException>          _ParsingExceptions;

        /// <summary>
        /// The original list of tokens passed to the parsing function in the original order
        /// </summary>
        public  IEnumerable<string>              AllTokens          {get {return _AllTokens.AsEnumerable();}}
        private List<string>                    _AllTokens;

        /// <summary>
        /// Any tokens which were not part of a named parameter, in the order they appeared
        /// in the original token list
        /// </summary>
        public  IEnumerable<string>              UndecoratedTokens  {get {return _UndecoratedTokens.AsEnumerable();}}
        private List<string>                    _UndecoratedTokens;

        /// <summary>
        /// The names of recognized tokens which were assigned into the settings
        /// </summary>
        public  IEnumerable<string>              AssignedTokens {get {return _AssignedTokens.AsEnumerable();}}
        private HashSet<string>                 _AssignedTokens;

        public CmdTailContext(IEnumerable<string> Tokens, IEnumerable<string> Undecorated, IEnumerable<CmdTailException> Exceptions, IEnumerable<string> AssignedTokens)
        {
            _AllTokens         = new List<string>(Tokens);
            _UndecoratedTokens = new List<string>(Undecorated);
            _ParsingExceptions = new List<CmdTailException>(Exceptions);
            _AssignedTokens    = new HashSet<string>(AssignedTokens);
        }
    }

    /// <summary>
    /// Interface for a simple binding command parameters to a command-like object 
    /// </summary>
    public interface ICmdTailCommand
    { //======================================================================
        /// <summary>
        /// Indicates if the object implementing ICmdTailCommand considers itself valid
        /// and runnable in a given context (source tokens, exceptions, etc.)
        /// </summary>
        /// <param name="ctx">Context object. Contains the original tokens, left-over tokens, and any exceptions found when parsing.</param>
        /// <returns></returns>
        bool IsValid(CmdTailContext ctx);

        /// <summary>
        /// Command pattern "execute" method to allow the data and functionality to live in the
        /// same object. This should perform whatever the user requested by making this command
        /// valid.
        /// </summary>
        /// <param name="o">Any command-specific data provided by the application (and not by the command line)</param>
        /// <returns>True if the command executed successfully, false otherwise</returns>
        bool Execute(object o);
    }

    /// <summary>
    /// A returned state after parsing a command line against a given settings object. It contains 
    /// the settings found, any errors encountered, any "left over" tokens, and the original tokens
    /// from the command line construction. By using this, the client knows which set of exceptions
    /// went with which results.
    /// </summary>
    /// <typeparam name="T">The type of settings object filled in</typeparam>
    public class ParsedCmdTail<T>
    { //======================================================================
    // No XML comment for these because I can't think of anything useful to say, Just move on
    #pragma warning disable 1591
            public T                       Settings          {get; protected set;}
            public CmdTailContext          Context           {get; protected set;}
    #pragma warning restore 1591

    #region ctor        
        /// <summary>
        /// Constructs a new ParsedCmdTail. Clients of the library should have no reason to do this.
        /// </summary>
        /// <param name="settings">A filled-in settings object</param>
        /// <param name="exceptions">A collection of exceptions encountered during parsing</param>
        /// <param name="tokens">The collection of tokens parsed. This should be the same as passed to the constructor of the CmdTail instance</param>
        /// <param name="undecorated">A collection of tokens that didn't match anything, but didn't look like settings. These may be interpreted however the client wishes.</param>
        public ParsedCmdTail(T settings, IEnumerable<CmdTailException> exceptions, IEnumerable<string> tokens, IEnumerable<string> undecorated, IEnumerable<string> assignedtokens)
        { //======================================================================
            Settings = settings;
            Context  = new CmdTailContext(Tokens: tokens, Undecorated: undecorated, Exceptions: exceptions, AssignedTokens: assignedtokens);
        }
    #endregion
    }
    
    /// <summary>
    /// Central interface to the command parsing library. Construct a CmdTail on
    /// your command arguments and call ParsedCmdTail to try and fill out structures
    /// </summary>
    public class CmdTail
    { //======================================================================
    #region State
        /// <summary>
        /// Array of characters to count as switchchars. The default is {'-','/'}
        /// </summary>
        public  static IEnumerable<char> SwitchChars {get {return _SwitchChars.AsEnumerable();} set {_SwitchChars = value.ToArray();}}
        private static char[] _SwitchChars = {'/', '-'};
        
        private Queue<string>           _Q                 = null;
        private HashSet<string>         _AssignedTokens    = new HashSet<string>();
        private List<string>            _UndecoratedTokens = new List<string>();
        private List<CmdTailException>  _ParsingExceptions = new List<CmdTailException>();

        /// <summary>
        /// Determines whether an exception will be thrown if a command line
        /// parameter is found that does not match a field in the settings structure
        /// Defaults to true
        /// </summary>
        public bool IgnoreUnknownParameters     {get; set;}
        
        /// <summary>
        /// Determines whether an exception will ever be thrown by the parsing library.
        /// Whether exceptions are thrown or not, the ParsingExceptions collection will
        /// contain all exceptions found while parsing (up until the parsing exited)
        /// Defaults to false
        /// </summary>
        public bool IgnoreAllErrors             {get; set;}
    #endregion
    #region ctor
        /// <summary>
        /// Constructs a new CmdTail object. Make a new instance whenever you have a different
        /// set of command parameters to handle.
        /// </summary>
        /// <param name="Tokens">An array or list of command line tokens, in the same format as passed to the Main function of a command line app</param>
        public CmdTail(params string[] Tokens)
        {   //=================================================================
            _Q = new Queue<string>(Tokens.AsEnumerable());
            IgnoreUnknownParameters = true;
            IgnoreAllErrors         = false;
        }

        /// <summary>
        /// Constructs a new CmdTail object from a string of tokens
        /// </summary>
        /// <param name="s">A string of whitespace separated tokens</param>
        /// <returns></returns>
        public static CmdTail Create(string s)                      {return new CmdTail(s.Split(' '));}

        /// <summary>
        /// Constructs a new CmdTail object from a string of tokens
        /// </summary>
        /// <param name="s">A string of tokens</param>
        /// <param name="sep">A character which separates the tokens</param>
        /// <returns></returns>
        public static CmdTail Create(string s, char sep)            {return new CmdTail(s.Split(sep));}

        /// <summary>
        /// Constructs a new CmdTail object from a string of tokens
        /// </summary>
        /// <param name="s">A string of tokens</param>
        /// <param name="asep">An array or parameter list of characters which separate tokens in the string</param>
        /// <returns></returns>
        public static CmdTail Create(string s, params char[] asep)  {return new CmdTail(s.Split(asep));}

    #endregion
    #region Help Helpers        
        /// <summary>
        /// Checks if the command line looks like a relatively standard request for help
        /// This finds things like "-help", "--h", "/?", etc.
        /// </summary>
        /// <returns>true if it looks like a help request</returns>
        public bool IsHelpRequest()
        {   //=================================================================
            string first = _Q.FirstOrDefault();
            if (first.IsEmpty()) return true;

            // Match:
            // Start with nothing, "-", "--", or "/"
            // Then require "h", "help", or "?"
            // and be case insensitive about it
            Regex rx = new Regex(@"(?i:^(--?|/)?(h(elp)?\b|\?\s*$))");
            return rx.IsMatch(first);
        }

        /// <summary>
        /// Finds any category of help passed to a standard help request, such as
        /// "prog -help commands"
        /// </summary>
        /// <example><![CDATA[
        /// enum HelpCategory {General, Commands, Examples, Errors}
        /// HelpCategory category = tail.HelpRequestCategory<HelpCategory>();
        /// ]]></example>
        /// <typeparam name="E">An enumeration with the names of the categories</typeparam>
        /// <exception>InvalidOperationException if the command tail does not seem to be a help request</exception>
        /// <returns>The matching enumeration member if found. Otherwise, the default enumeration member</returns>
        public E HelpRequestCategory<E>() where E : struct
        {   //=================================================================
            if (!IsHelpRequest()) throw new InvalidOperationException("This is not a help request, so no help category is available");

            // The default return is the default of the enumeration
            E ret = default(E);
            
            // If we have a second parameter, try to match it to the enum for a help type
            if (_Q.Count() > 1)
            {             
                string Topic = _Q.Skip(1).First();
            
                if (Enum.TryParse(Topic, !ParseIgnoreCase, out ret)) return ret;
                if (Enum.TryParse(Topic,  ParseIgnoreCase, out ret)) return ret;

                // Enum couldn't parse it, so let's look for substrings
                // Note that we don't have a way to disallow substring matching for help topics
                // Enum.GetNames returns strings, so we go back to Enum.Parse with the full name to get an E
                IEnumerable<string> Topics = from ns in Enum.GetNames(typeof(E)) where ns.NoCaseStartsWith(Topic) select ns;
                if (Topics.Count() == 1) return (E)Enum.Parse(typeof(E), Topics.First());
            }
            
            return ret;
        }
    #endregion
    #region String Matching
        // Functions to compare command line strings to known strings in the settings set. These allow
        // using substrings matching ("/f" for "/filepath"), case insensitive matching ("/date" for "/Date"),
        // and alternate names ("/ipw" for "/IgnorePatternWhitespace"). It also allows turning these options
        // off on a setting-by-setting basis.
        private static bool CompareStrings(string lhs, string rhs, bool CaseSensitive, bool AllowSubstringMatch)
        {   //=================================================================
            if ( CaseSensitive &&  AllowSubstringMatch) return lhs.StartsWith(rhs, StringComparison.CurrentCulture);
            if ( CaseSensitive && !AllowSubstringMatch) return lhs == rhs;
            if (!CaseSensitive &&  AllowSubstringMatch) return lhs.NoCaseStartsWith(rhs);
            return lhs.NoCaseEquals(rhs); // !CaseSensitive && !AllowSubstringMatch
        }
        private static bool MatchSettingName(string Setting, string Compare, CmdTailSettingAttribute attr)
        {   //=================================================================
            attr = attr ?? new CmdTailSettingAttribute();
            if (CompareStrings(Setting, Compare, attr.NameCaseSensitive, attr.NameAllowSubstring)) return true;
            if (attr.AlternateNames != null)
            {
                return attr.AlternateNames.Any(Alt => CompareStrings(Alt, Compare, attr.NameCaseSensitive, attr.NameAllowSubstring));
            }
            return false;
        }
        private static bool MatchSettingValue(string Setting, string Compare, CmdTailSettingAttribute attr)
        {   //=================================================================
            // Values don't have alternate strings (at this time)
            attr = attr ?? new CmdTailSettingAttribute();
            return CompareStrings(Setting, Compare, attr.ValueCaseSensitive, attr.ValueAllowSubstring);            
        }       
    #endregion
    #region Field Handling
        private const bool InheritAttributes = true;
        // Simple function to get the FieldInfo for a name that matches (see the string matching options above)
        private static FieldInfo FindField<T>(string Name)
        {   //=================================================================
            Type t = typeof(T);
            IEnumerable<string> Names = from   f in t.GetFields() 
                                        let    a = f.GetCustomAttributes(InheritAttributes).FirstOrDefault(att => att is CmdTailSettingAttribute)
                                        where  f.IsPublic && MatchSettingName(f.Name, Name, a as CmdTailSettingAttribute)
                                        select f.Name;
            if (Names.Count() == 0) throw new UnknownSettingException(Name);
            if (Names.Count() == 1) 
            {
                // Get the name from the field list so the casing is exact
                return (from   f in t.GetFields()
                        where  f.Name == Names.First()
                        select f).First();
            }
            
            // More than one could match. Return an *exact* match if one exists.
            // This is for options like "/time /timestamp"
            IEnumerable<FieldInfo> fis = from f in t.GetFields() where f.Name == Name select f;
            if (fis.Count() == 1) return fis.First();

            throw new AmbiguousSettingNameException(Name, Names.AsEnumerable());            
        }

        // Special case this one for very little good reason, but it is very a common case
        private static void SetBoolField<T>(ref T o, string Name, bool Value)
        {   //=================================================================
            FieldInfo fi = FindField<T>(Name);
            fi.SetValue(o, Value);
            return;
        }

        /// <summary>
        /// Add a functor to convert the text on the command line to the field type
        /// All fields of this type will use this function to convert
        /// </summary>
        /// <param name="t">The field type you want to perform a custom coercion for</param>
        /// <param name="fn">A Func object or lambda to do the conversion. It takes a string and should return the type</param>
        public void AddValueCoercer(Type t, Func<string, object> fn) {_Coercers.Add(t, fn);}

        /// <summary>
        /// Remove the custom coercion for a type and restore the standard coercion rules
        /// </summary>
        /// <param name="t">The type you want to remove the coercion function for</param>
        public void RemoveValueCoercer(Type t)                       {_Coercers.Remove(t);}
        
        private Dictionary<Type, Func<string, object>> _Coercers = new Dictionary<Type,Func<string, object>>()
        {   //=================================================================
            // Examples:
            //{typeof(int),      s => int.Parse(s)},
            //{typeof(DateTime), s => DateTime.Parse(s)},
            //{typeof(TimeSpan), s => TimeSpan.Parse(s)},
            //{typeof(float),    s => float.Parse(s)},
            //{typeof(double),   s => double.Parse(s)}
            //{typeof(string[]), s => s.Split('+')}
        };
        
        private const bool ParseIgnoreCase = true;
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification="We are converting the exception to a new type; the general catch is intentional")]
        private void SetField<T>(ref T o, string Name, string Value)
        {   //=================================================================
            FieldInfo fi = FindField<T>(Name);
            
            // -----------------------------------------------------------
            // Use a coercer if there is one
            if (_Coercers.ContainsKey(fi.FieldType)) 
            {
                // Wrap any exceptions from the coercer in an InvalidSettingValue exception
                try
                {
                    fi.SetValue(o, _Coercers[fi.FieldType](Value));
                }
                catch (Exception e)
                {
                    throw new InvalidSettingValueException(Name, Value, fi.FieldType, e);
                }
                return;
            }

            // -----------------------------------------------------------
            // Enums get special handling
            if (fi.FieldType.IsEnum)
            {
                try {fi.SetValue(o, Enum.Parse(fi.FieldType, Value, ParseIgnoreCase)); return;} catch {}

                // Enum.Parse threw, so we have to check for substrings
                string[] Names = Enum.GetNames(fi.FieldType);
                IEnumerable<string> MatchingNames = from   n in Names.AsEnumerable()
                                                    let    a = fi.GetCustomAttributes(InheritAttributes).FirstOrDefault(att => att is CmdTailSettingAttribute) as CmdTailSettingAttribute
                                                    where  MatchSettingValue(n, Value, a)
                                                    select n;
                if (MatchingNames.Count() == 0) throw new UnrecognizedValueException(Name, Value);
                if (MatchingNames.Count() > 1)  throw new AmbiguousSettingValueException(Name, Value, MatchingNames);
                fi.SetValue(o, Enum.Parse(fi.FieldType, MatchingNames.First()));
                return;
            }
            
            // -----------------------------------------------------------
            // Okay. Not an enum. Let's look for a static method named "Parse", 
            // taking one string. The .NET library uses that a lot.
            MethodInfo mi = fi.FieldType.GetMethod("Parse", new Type[] {typeof(string)});
            if (mi != null && mi.IsStatic)
            {
                object parsed = mi.Invoke(null, new object[] {Value});
                fi.SetValue(o, parsed);
                return;
            }

            // -----------------------------------------------------------
            // Failing all of that, let's just see if assigning works
            fi.SetValue(o, Value); // Set it as a string and hope
            return;
        }
        
        private static string DefaultParameterName<T>()
        {   //=================================================================
            Type t = typeof(T);
            IEnumerable<string> Names = from   f in t.GetFields() 
                                        let    a = f.GetCustomAttributes(InheritAttributes).FirstOrDefault(att => att is CmdTailSettingAttribute) as CmdTailSettingAttribute
                                        where  a != null && f.IsPublic && a.DefaultArgument == true
                                        select f.Name;
            if (Names.Count() == 1) return Names.First();
            return null;
        }

        private void AssignDefaultParameter<T>(ref T ret)
        {   //=================================================================
            string def = DefaultParameterName<T>();
            if (null == def) return;
            if (_UndecoratedTokens.Count == 0) return;
            if (_AssignedTokens.Contains(def)) return;

            string val = _UndecoratedTokens.First();
            SetField(ref ret, def, val);
            _UndecoratedTokens.RemoveAt(0);
            _AssignedTokens.Add(def);
        }
    #endregion
    #region Public Interface
        // Not really part of the *public* interface, but that's the only place it's used
        // This is here to make switch statements and comparisons below more readable
        // An "explicit" bool is something like "/b+", an "Attached" bool is "/b=true"
        // The "other" category includes "implicit" bools ("/b" meaning true) and
        // "detached" books ("/b true"), but we won't know which at the time we're
        // using this enumeration.
        private enum BoolFormat {Explicit, AttachedString, Other}

        /// <summary>
        /// Parses the command tokens into a given structure and returns the complete state.
        /// </summary>
        /// <typeparam name="T">Class to populate with settings</typeparam>
        /// <returns>A ParsedCmdTail object with the settings, the command tokens, any exceptions, and any loose parameters</returns>
        public ParsedCmdTail<T> Parse<T>() where T : new() {return Parse(new T());}
        
        /// <summary>
        /// Parses the command tokens into a given structure and returns the complete state, with custom defaults supplied
        /// </summary>
        /// <typeparam name="T">Class to populate with settings</typeparam>
        /// <param name="initial">Initial/default values. Must not be null</param>
        /// <returns>A ParsedCmdTail object with the settings, the command tokens, any exceptions, and any loose parameters</returns>
        public ParsedCmdTail<T> Parse<T>(T initial)
        {   //=================================================================
            if (null == initial) throw new ArgumentNullException("initial");
            
            // Copy the queue to preserve the member variable. We can reuse it if we're called again. Meanwhile, we can party on the copy.
            // We need .AsEnumerable to avoid reference copying
            Queue<string> q = new Queue<string>(_Q.AsEnumerable()); 
            
            _ParsingExceptions = new List<CmdTailException>();
            _UndecoratedTokens = new List<string>();
            _AssignedTokens    = new HashSet<string>();
                        
            T ret = initial; // It would be nice to know that T is newable, but this version doesn't constrain that. We may be using a reference copy here
            while (q.Count > 0)
            {
                string Token = q.Dequeue();

                try
                {
                    if (Token.IsEmpty()) {throw new EmptyTokenException();}

                    // The only special case so far is "-", which could mean stdin
                    if ("-".Equals(Token) && HasStdInField<T>())
                    {
                        ret = SetStdInField(ret);
                    }
                    else
                    { 
                        bool bTokenIsDecorated = Token.BeginsWithAny(_SwitchChars);
                        switch (bTokenIsDecorated)
                        {
                            case true:
                                HandleDecoratedToken(ref ret, q, Token);
                                break;

                            case false:
                                _UndecoratedTokens.Add(Token);
                                break;
                        }
                    }
                }
                catch (UnknownSettingException cex)
                {
                    _ParsingExceptions.Add(cex);
                    if (!IgnoreUnknownParameters && !IgnoreAllErrors) throw;
                }
                catch (CmdTailException cex)
                {
                    _ParsingExceptions.Add(cex);
                    if (!IgnoreAllErrors) throw;
                }
            } // Looping over q
            
            AssignDefaultParameter(ref ret);
            return new ParsedCmdTail<T>(ret, _ParsingExceptions.AsEnumerable(), _Q.AsEnumerable(), _UndecoratedTokens.AsEnumerable(), _AssignedTokens.AsEnumerable());
        }

        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification="The formal type parameter is easier on the caller than consing up an object")]
        public bool ExecuteIfValid<T>(object o) where T : ICmdTailCommand, new()
        { //=================================================================
            ParsedCmdTail<T> pct = Parse<T>();
            if (pct.Settings.IsValid(pct.Context))
            {
                pct.Settings.Execute(o);
                return true;
            }
            return false;
        }

        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification="The formal type parameter is easier on the caller than consing up an object")]
        public bool ExecuteIfValid<T>() where T : ICmdTailCommand, new()
        { //=================================================================
            return ExecuteIfValid<T>(null);
        }
    #endregion
    #region Token Handling
        // Why isn't this part of the bool class yet?
        private static bool? StringToBool(string s)
        {   //=================================================================
            bool bRet = false;
            return bool.TryParse(s, out bRet) ? (bool?)bRet : null;
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification="The exception is informational and we want to hide it from our caller")]
        private static bool HasStdInField<T>() 
        {   //====================================================================================
            try
            {
                FieldInfo fi = FindField<T>("StdIn");
                return fi.FieldType == typeof(bool);
            }
            catch {;}
            return false;
        }
        
        private static T SetStdInField<T>(T ret) 
        {   //====================================================================================
            if (HasStdInField<T>()) SetBoolField(ref ret, "StdIn", true); // Yes, we have now searched for this field 3 times in a row.
            return ret;
        }

        // Static so it only gets compiled once. It will get cached, but this is still faster and there is no good reason to recreate it.
        private Regex _rxToken = new Regex(@"^(?<prebools>[+-]*)(?<name>[a-zA-Z][_a-zA-Z0-9]*)(?:(?<bools>[+-]*)|(?<assign>[:=])(?<value>.*))(?!\S)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
        // Factored out of Parse for clarity
        private void HandleDecoratedToken<T>(ref T ret, Queue<string> q, string Token)
        {   //====================================================================================
            // Break out the token into parts
            Match ParsedToken = _rxToken.Match(Token.TrimStart(_SwitchChars));  //TODO: This should handle -- better
            string Name     = ParsedToken.Groups["name"].Value;           // The name of the setting: the set of allowed characters at the start of the token
            string PreBools = ParsedToken.Groups["prebools"].Value;       // Any +- characters before the name: "/+a /-r"
            string Bools    = ParsedToken.Groups["bools"].Value;          // Any +- characters after the name: "/a+ /r-"
            string Value    = ParsedToken.Groups["value"].Value;          // The concatenated string value: "/Name=Value" or "/Name:Value"
            string Assign   = ParsedToken.Groups["assign"].Value;         // Any assignment character used in the token, like the equals in /A=B

            // Some options:
            //      <no switchchar><anything>                   Undecorated value. Handled elsewhere
            //      <switchchar><name>                          If "name" is a bool, implicit true
            //      <switchchar><name><plusminus>               If "name" is a bool, true or false
            //      <switchchar><name><colon or equals>         If "name" is not a bool, clear the value to its default
            //      <switchchar><name><colon or equals><value>  Coerce "value" to the type of "name"
            //      <switchchar><name> <no switchchar><value>   Coerce "value" to the type of "name"

            // Must have a field name to assign to
            if (Name.IsEmpty()) throw new UnNamedTokenException();
            
            FieldInfo fi = FindField<T>(Name);
            if (fi.FieldType == typeof(bool))
            {
                // May have at most one of Bools, PreBools, or Value
                int count = 0;
                if (Bools.IsNotEmpty())     count += 1;
                if (PreBools.IsNotEmpty())  count += 1;
                if (Value.IsNotEmpty())     count += 1;
                if (count > 1) throw new MalformedTokenException(Token);

                HandleBoolToken<T>(ref ret, q, Name, PreBools + Bools, Value);
            }
            else
            {
                // If we don't have a Value but we *do* have an Assign, they want to assign null/empty: "/logfile="
                // If we don't have an assign then we want to check the next token to see if it's our value
                // There should be no way to have a Value at this point without having an Assign unless we have a very confused bool/non-bool situation
                if (Assign.IsEmpty())
                {
                    // We have a name. See if we have a value. If not, try the next token on the queue
                    if (q.Count() == 0) throw new SettingMissingValueException(Name);     // "/s" but s isn't a bool and there is no value left on the tail

                    string Next = q.Peek();
                    Match ParsedNext = _rxToken.Match(Next.TrimStart(_SwitchChars));

                    // Bail out if the next token starts with a switch character and seems to be a new switch (i.e., it has a name)
                    // This will allow:
                    //      /dir /
                    //      /offset -12
                    // But not
                    //      /dir /home
                    //      /offset -one
                    if (Next.BeginsWithAny(_SwitchChars) && 
                       ParsedNext.Groups["name"].Value.IsNotEmpty()) 
                    {
                        throw new SettingMissingValueException(Name);     // "/s /t" but s isn't a bool
                    }
                    
                    Value = q.Dequeue();    // Consume the token from the queue (and stick it in Value to be assigned)
                }
                SetField(ref ret, Name, Value);
            }
            _AssignedTokens.Add(fi.Name);
        }

        // Extracted to clarify logic in HandleDecoratedToken
        private static void HandleBoolToken<T>(ref T ret, Queue<string> q, string Name, string Bools, string Value)
        {   //====================================================================================
            BoolFormat bf = (Bools.IsNotEmpty()) ? BoolFormat.Explicit
                          : (Value.IsNotEmpty()) ? BoolFormat.AttachedString
                          :                        BoolFormat.Other;
            bool? bv = StringToBool(Value);
            switch (bf)
            {
                case BoolFormat.Explicit:       SetBoolField(ref ret, Name, Bools.Last() == '+'); break;  // "/b+" "/b-"
                case BoolFormat.AttachedString: SetBoolField(ref ret, Name, bv ?? false);         break;  // "-b=true" or "-b:false" or "-b=confused"

                case BoolFormat.Other:
                    // It's a bool and we have no matching value yet
                    // Check the next item in the queue
                    string Next = q.Count() > 0 ? q.Peek() : string.Empty;
                    bv = StringToBool(Next);
                    if      (Next.IsEmpty())                   {SetBoolField(ref ret, Name, true);}                   // "-b" (end of arguments)
                    else if (Next.BeginsWithAny(_SwitchChars)) {SetBoolField(ref ret, Name, true);}                   // "-b -other"
                    else if (bv.HasValue)                      {q.Dequeue(); SetBoolField(ref ret, Name, bv.Value);}  // "-b true" or "-b false", and consume the token from the queue
                    else                                       {SetBoolField(ref ret, Name, true);}                   // "-b gibberish", so treat as an implicit true
                    break;
            }
        }
    #endregion
    }
}
